
package org.hackystat.ui.sensordatabrowser.server;

import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.xml.datatype.XMLGregorianCalendar;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.client.SensorBaseClientException;
import org.hackystat.utilities.tstamp.Tstamp;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectIndex;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectRef;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectSummary;
import org.hackystat.sensorbase.resource.projects.jaxb.SensorDataSummary;
import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.ui.sensordatabrowser.client.common.Event;
import org.hackystat.ui.sensordatabrowser.client.common.EventItemsPage;
import org.hackystat.ui.sensordatabrowser.client.common.ProjectParcel;
import org.hackystat.ui.sensordatabrowser.client.common.RPCException;
import org.hackystat.ui.sensordatabrowser.client.common.RPCRemoteService;

/**
 * Provides remote asynchronous service for fetching sensor data.
 * @author David J. Nickles
 */
public class RPCServiceImpl extends RemoteServiceServlet implements RPCRemoteService {

  /* Class variables for serialized object id */
  private static final long serialVersionUID = 11121936L;
  /** The sensor properties instance. */
  private Properties sensorProperties; 
  
  /**
   * Return a formatted description of the requested sensor data on the specified interval.
   * @param host the URI for the SensorBase.
   * @param project the project of interest.
   * @param user user to authentication.
   * @param password user password for authentication.
   * @param xmlTstamp XML-format timestamp for start of data interval.
   * @return List of Events that describe this day of events.
   * @throws RPCException if problems occur during the Sensorbase procedure call.
   */
  public ArrayList<Event> getDaySummary(String host, String project, 
    String user, String password, String xmlTstamp) throws RPCException {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    ArrayList<Event> eventList = new ArrayList<Event>();
    String params = "URI: " + host + ", Project: " + project + ", User: " + user
      + ", Timestamp: " +  xmlTstamp;
    log("SDB getSummary: entering method\n params:" + params + '\n');
    try {
      XMLGregorianCalendar startTime = Tstamp.makeTimestamp(xmlTstamp);
      XMLGregorianCalendar endTime = 
        Tstamp.incrementSeconds(Tstamp.incrementDays(startTime, 1), -1);
      SensorBaseClient client = new SensorBaseClient(host, user, password);
      ProjectSummary summary = null;
      try {
        summary = client.getProjectSummary(user, project, startTime, endTime);
      }
      catch (SensorBaseClientException e) {
        log("SDB: getDaySummary: " + e.getMessage() + ", occured calling projectSummary for " 
          + startTime.toXMLFormat() + " to " + endTime.toXMLFormat());
      }
      if (summary != null &&
        !summary.getSensorDataSummaries().getNumInstances().equals(BigInteger.ZERO)) {
        for (int j = 0; j < 24; j++) {
          endTime = Tstamp.incrementSeconds(Tstamp.incrementHours(startTime, 1), -1);
          try {
            //log("SDB: getDaySummary outer-loop for " 
            //    + startTime.toXMLFormat() + " to " + endTime.toXMLFormat());
            summary = client.getProjectSummary(user, project, startTime, endTime);
          }
          catch (SensorBaseClientException e) {
            log("SDB: getDaySummary: " + e.getMessage() + ", occured calling projectSummary for " 
              + startTime.toXMLFormat() + " to " + endTime.toXMLFormat());
          }
          if (summary.getSensorDataSummaries().getNumInstances().equals(BigInteger.ZERO)) {
            startTime = Tstamp.incrementHours(startTime, 1);
          }
          else {
            for (int i = 0; i < 60; i++) {
              endTime = Tstamp.incrementSeconds(Tstamp.incrementMinutes(startTime, 1), -1);
              //log("SDB: getDaySummary inner-loop for " 
              //    + startTime.toXMLFormat() + " to " + endTime.toXMLFormat());
              ProjectSummary minuteSummary = null;
              try {
                minuteSummary = client.getProjectSummary(user, project, startTime, endTime);
                if (!minuteSummary.getSensorDataSummaries().getNumInstances().equals(
                  BigInteger.ZERO)) {
                  for (SensorDataSummary dataSummary : 
                    minuteSummary.getSensorDataSummaries().getSensorDataSummary()) {
                    eventList.add((EventHandler.makeEvent(startTime, dataSummary)));
                  }
                }
              }
              catch (SensorBaseClientException e) {
                log("SDB: getDaySummary (" + e.getMessage() 
                  + ") occured calling getProjectSummary for " 
                  + startTime.toXMLFormat() + " to " + endTime.toXMLFormat());
              }
              startTime = Tstamp.incrementMinutes(startTime, 1);
              //log("SDB: getDaySummary endloop with " + startTime.toXMLFormat());
            }
          }
        }
      }
    } 
    catch (Exception e) {
      log("SDB getDaySummary: error: " + e.getMessage());
    }
    stopwatch.stop();
    log("SDB getDaySummary: returning " + eventList.size() + " new event entries.\n"
        + "exiting method with execution time of " + stopwatch + '\n');
    return eventList; 
  }
  
  /**
   * This method filters the SensorDataIndex to only refs of the given tool.
   * @param index the SensorDataIndex to filter.
   * @param tool the tool to filter by.
   * @return the new SensorDataIndex.
   */
  private SensorDataIndex filterIndex(SensorDataIndex index, String tool) {
    SensorDataIndex resultList = new SensorDataIndex();
    for (SensorDataRef ref : index.getSensorDataRef()) {
      if (tool.equals(ref.getTool())) {
        resultList.getSensorDataRef().add(ref);
      }
    }
    return resultList;
  }
  
  /**
   * Return a formatted description of the requested sensor data within the specified event.
   * @param uri the URI for the SensorBase
   * @param project the project of interest
   * @param user user to authentication
   * @param password user password for authentication
   * @param event to get items within.
   * @param reqPage the requested page.
   * @return EventItemsPage of the requested sensor data.
   * @throws RPCException if problems occur during the Sensorbase procedure call
   */
  public EventItemsPage getEventItems(String uri, String project, String user, 
    String password, Event event, EventItemsPage reqPage) throws RPCException {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    EventItemsPage page = new EventItemsPage();
    ArrayList<ArrayList<String>> dataList = new ArrayList<ArrayList<String>>();
    String params = "URI: " + uri + ", Project: " + project + ", User: " + user
      + ", Timestamp: " +  event.getTimestamp() + ", SDT: " + event.getType() 
      + ", Tool: " +  event.getTool() + ", Page: " +  reqPage.getPageNumber();
    log("SDB getEventItems: entering method\n params:" + params + '\n');
    try {
      XMLGregorianCalendar start = Tstamp.makeTimestamp(event.getTimestamp());
      XMLGregorianCalendar end = Tstamp.incrementSeconds(Tstamp.incrementMinutes(start, 1), -1);
      SensorBaseClient client = new SensorBaseClient(uri, user, password);
      SensorDataIndex index = filterIndex(client.getProjectSensorData(user, project, start, end, 
        event.getType()), event.getTool());
      log("SDB getEventItems: SensorDataIndex retrieved from " + start.toXMLFormat() + " to " 
          + end.toXMLFormat() + " with " + index.getSensorDataRef().size() + " items.\n");
      page.setPageNumber(reqPage.getPageNumber());
      int startIndex = reqPage.getPageNumber() * EventItemsPage.PAGESIZE;
      int pageTotal = startIndex + EventItemsPage.PAGESIZE;
      List<SensorDataRef> refList = index.getSensorDataRef();
      if (pageTotal > refList.size()) {
        pageTotal = refList.size();
        page.setMore(false);
      }
      else {
        page.setMore(true);
      }
      log("SDB getEventItems: get index from " + startIndex + " to " + pageTotal + "\n");
      for (int i = startIndex; i < pageTotal; i++) {
        SensorDataRef ref = refList.get(i);
        dataList.add(formatSensordata(client.getSensorData(ref)));
      }
      page.setStatus(Boolean.TRUE.toString());
    } 
    catch (Exception e) {
      log("SDB getEventItems: error: " + e.getMessage());
      page.setStatus(e.getMessage());
    }
    page.setDataList(dataList);
    stopwatch.stop();
    log("SDB getEventItems: return stats: " + page.getPageNumber() + ", " 
        + page.getStatus() + ", " + page.isMore() + ", " + '\n');
    log("SDB getEventItems: returning " + dataList.size() + " new event entries.\n"
      + "exiting method with execution time of " + stopwatch + '\n');
    return page; 
  }
    
  /**
   * Creates an array of strings for the sensor data instance in the SensorData instance.
   * @param sensordata A JAXB unmarshalled SensorData instance
   * @return String A formatted line of sensor data attributes of sensor data to present in the UI
   * @throws Exception If problems occur during the Sensorbase procedure call
   */
  private ArrayList<String> formatSensordata(SensorData sensordata) throws Exception {
    ArrayList<String> list = new ArrayList<String>();
    list.add(SimpleDateFactories.getInstance().getBodyfactory().format(
        sensordata.getTimestamp().toGregorianCalendar().getTime()) + " " 
        + sensordata.getSensorDataType() + " " + sensordata.getTool());
    list.add("Timestamp=" + sensordata.getTimestamp().toXMLFormat());
    list.add("SensorDataType=" + sensordata.getSensorDataType());
    list.add("Tool=" + sensordata.getTool());
    list.add("LastMod=" + sensordata.getLastMod().toXMLFormat());
    list.add("Runtime=" + sensordata.getRuntime().toXMLFormat());
    list.add("Resource=" + sensordata.getResource());
    list.add("Owner=" + sensordata.getOwner());
    for (Property p : sensordata.getProperties().getProperty()) {
      StringBuffer property = new StringBuffer();
      property.append(p.getKey());
      property.append('=');
      property.append(p.getValue());
      list.add(property.toString());
    }
    return list;
  }
  
  /**
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param cal timestamp as XMLGregorianCalendar object
   * @return the quantized timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar quantize(XMLGregorianCalendar cal) {
    cal.setHour(0);
    cal.setMinute(0);
    cal.setSecond(0);
    cal.setMillisecond(0);
    return cal;
  }
  
  /**
   * Computes the first day of the given month.
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param year the number of the year as an integer
   * @param month the number of the month as an integer
   * @return the first of the month timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar getFirstOfMonth(int year, int month) {
    XMLGregorianCalendar cal = Tstamp.makeTimestamp();
    cal.setYear(year);
    cal.setMonth(month);
    cal.setDay(1);
    cal = quantize(cal);
    return cal;
  }

  /**
   * Return a formatted description of the requested sensor data on the specified interval.
   * @param uri the URI for the SensorBase
   * @param project the project of interest
   * @param user user to authentication
   * @param password user password for authentication
   * @param year of interval
   * @param month of interval
   * @return Boolean array of days with data
   * @throws RPCException if problems occur during the Sensorbase procedure call
   */
  public ArrayList<Boolean> getOverview(String uri, String project, String user, String password, 
    int year, int month) throws RPCException {
    XMLGregorianCalendar start = getFirstOfMonth(year, month);
    String params = "URI: " + uri + ", Project: " + project + ", User: " + user 
      + ", year: " +  year + ", month: " + month + ", start: " +  start.toXMLFormat() + '\n';
    log("SDB entering method getOverview\n" + params);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ArrayList<Boolean> dayList = new ArrayList<Boolean>();
    GregorianCalendar cal = start.toGregorianCalendar();
    int totalDays = cal.getActualMaximum(GregorianCalendar.DAY_OF_MONTH);
    log("SDB getOverview for " + month + "/" + year + ", #days: " + totalDays + '\n');
    SensorBaseClient client = new SensorBaseClient(uri, user, password);
    for (int i = 0; i < totalDays; i++) {
      XMLGregorianCalendar end = Tstamp.incrementSeconds(Tstamp.incrementDays(start, 1), -1);
      ProjectSummary summary = null;
      try {
        summary = client.getProjectSummary(user, project, start, end);
      }
      catch (SensorBaseClientException e) {
        log("SDB: getOverview: " + e.getMessage() + ", occured calling projectSummary for period " 
          + start.toXMLFormat() + " to " + end.toXMLFormat());
      }
      if (summary == null) {
        dayList.add(i, Boolean.FALSE);
      }
      else if (summary.getSensorDataSummaries().getNumInstances().equals(BigInteger.ZERO)) {
        dayList.add(i, Boolean.FALSE);
      }
      else {
        dayList.add(i, Boolean.TRUE);
      }
      start = Tstamp.incrementDays(start, 1);
    }
    stopwatch.stop();
    log("SDB getOverview exiting method with execution time of " + stopwatch + '\n');
    return dayList; 
  }
  
  /**
   * Returns true if the passed host is a SensorBase host.
   * 
   * @param host the URI for the SensorBase
   * @return Boolean indicating if the host is a valid sensorbase
   */
  public Boolean isHost(String host) {
    log("SDB checking isHost " + host + '\n');
    return SensorBaseClient.isHost(host);
  }

  /**
   * Returns true if the user and password is registered as a user with this host.
   * 
   * @param host the URI for the SensorBase
   * @param user user to authentication
   * @param password user password for authentication
   * @return Boolean indicating if the user and password are valid in the sensorbase
   */
  public Boolean isRegistered(String host, String user, String password) {
    log("SDB checking isRegistered, user: " + user + '\n');
    return SensorBaseClient.isRegistered(host, user, password);
  }

  /**
   * Registers the user with this host.
   * 
   * @param host the URI for the SensorBase
   * @param user user to authentication
   * @return Boolean indicating if the user and password are valid in the sensorbase
   */
  public Boolean registerUser(String host, String user) {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    log("SDB registerUser on sensorbase (" + host + ") for (" + user + ")\n");
    Boolean result = Boolean.TRUE;
    try {
      log("SDB registerUser: invoking static method on client");
      SensorBaseClient.registerUser(host, user);
      log("SDB registerUser: returned from static method on client");
    }
    catch (SensorBaseClientException e) {
      result = Boolean.FALSE;
      log("SDB registerUser error: " + e.getMessage());
    }
    stopwatch.stop();
    log("SDB registerUser: exiting method with an execution time of " + stopwatch + '\n');
    return result;
  }
  
  /**
   * Returns the version number from the Manifest file.
   * 
   * @return String the version number from the Manifest file.
   */
  public String version() {
    String result = "Development";
    try {
      URL manifestURL = getServletContext().getResource("/META-INF/MANIFEST.MF");
      if (manifestURL == null) {
        log("SDB: Unable to get Manifest file.\n");
        return result;
      }
      Manifest mf = new Manifest(manifestURL.openStream());
      Attributes attr = mf.getMainAttributes();
      result = attr.getValue("Implementation-Version");
      if (result == null) {
        log("SDB: Unable to get 'Implementation-Version' attribute from the Manifest.\n");
        return result;
      }
    } 
    catch (Exception e) {
      log("SDB error: " + e.getMessage() + '\n');
    }
    log("SDB: Checking manifest, version=" + result + '\n');
    return result; 
  }
  
  /**
   * Returns the host uri from the configuration file.
   * 
   * @return String the host uri.
   */
  public String getHost() {
    String result = "http://hackystat.org:9876/sensorbase";
    try {
      this.sensorProperties = new Properties();
      if (this.sensorProperties == null) {
        log("SDB props: Unable to get SDV properties file.\n");
        return result;
      }
      result = this.sensorProperties.getHackystatHost();
      if (result == null) {
        log("SDB props: Unable to get 'Host' attribute from the properties file.\n");
        return result;
      }
    } 
    catch (Exception e) {
      log("SDB props error: " + e.getMessage() + '\n');
    }
    log("SDB props success: host=" + result + '\n');
    return result; 
  }

  /**
   * Returns the index of all Projects from this server for this user..
   * @param uri the URI for the SensorBase.
   * @param user username (email address) for authentication.
   * @param password user password for authentication.
   * @return Returns the index of all Projects from this server for this user.
   * @throws RPCException if problems occur during the Sensorbase procedure call.
   */
  public ArrayList<ProjectParcel> getProjectParcels(String uri, String user, String password)
    throws RPCException {
    String params = "URI: " + uri + "User: " + user + '\n';
    log("PV getProjectParcels entering method with params:\n" + params);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ArrayList<ProjectParcel> result = new ArrayList<ProjectParcel>();
    try {
      SensorBaseClient client = new SensorBaseClient(uri, user, password);
      ProjectIndex index = client.getProjectIndex(user);
      for (ProjectRef ref : index.getProjectRef()) {
        Project p = client.getProject(ref);
        ProjectParcel projectParcel = new ProjectParcel();
        projectParcel.setName(p.getName());
        projectParcel.setDescription(p.getDescription());
        projectParcel.setOwner(p.getOwner());
        projectParcel.setMembers(p.getMembers().getMember());
        projectParcel.setInvitations(p.getInvitations().getInvitation());
        HashMap<String, String> propertMap = new HashMap<String, String>();
        for (org.hackystat.sensorbase.resource.projects.jaxb.Property property : 
          p.getProperties().getProperty()) {
          propertMap.put(property.getKey(), property.getValue());
        }
        projectParcel.setProperties(propertMap);
        projectParcel.setUriPatterns(p.getUriPatterns().getUriPattern());
        projectParcel.setStartTime(SimpleDateFactories.getInstance().getDatefactory().format(
          p.getStartTime().toGregorianCalendar().getTime()));
        projectParcel.setEndTime(SimpleDateFactories.getInstance().getDatefactory().format(
          p.getEndTime().toGregorianCalendar().getTime()));
        result.add(projectParcel);
        log("PV getProjectParcels: adding project " + projectParcel.getName());
      }
    } 
    catch (Exception e) {
      log("PV getProjectParcels: exception thrown: " + e.getMessage() + '\n');
    }
    stopwatch.stop();
    log("PV getProjectParcels exiting method with execution time of " 
      + stopwatch + '\n');
    return result; 
  }
}